/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License");  you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * http//www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 *
 * The Original Code is OntologyIndexer.
 *
 * The Initial Developer of the Original Code is University of Roma Tor Vergata.
 * Portions created by University of Roma Tor Vergata are Copyright (C) 2006.
 * All Rights Reserved.
 *
 * OntologyIndexer was developed by the Artificial Intelligence Research Group
 * (ai-nlp.info.uniroma2.it) at the University of Roma Tor Vergata
 * Current information about OntologyIndexer can be obtained at 
 * http//ai-nlp.info.uniroma2.it/software/...
 *
 */

package it.uniroma2.art.owlart.langsearch.index;

import it.uniroma2.art.owlart.exceptions.ModelAccessException;
import it.uniroma2.art.owlart.filter.DesiredLanguageLabelPredicate;
import it.uniroma2.art.owlart.langsearch.OWLSearchEngine;
import it.uniroma2.art.owlart.langsearch.index.config.IndexingConfiguration;
import it.uniroma2.art.owlart.langsearch.index.struct.AnalyzerMap;
import it.uniroma2.art.owlart.langsearch.index.struct.IndexWriterMap;
import it.uniroma2.art.owlart.model.ARTLiteral;
import it.uniroma2.art.owlart.model.ARTURIResource;
import it.uniroma2.art.owlart.models.OWLModel;
import it.uniroma2.art.owlart.navigation.ARTLiteralIterator;
import it.uniroma2.art.owlart.navigation.ARTURIResourceIterator;
import it.uniroma2.art.owlart.vocabulary.RDFResourceRolesEnum;
import it.uniroma2.art.owlart.vocabulary.RDFS;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.KeywordAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterators;

public class RDFIndexManager {

	// public static String _labelSplittingSeq = "|_|";
	public static String _labelSplittingSeq = "§";

	private String idxFieldLocalName = "localName";
	private String idxFieldURI = "URI";

	protected static Logger logger = LoggerFactory.getLogger(RDFIndexManager.class);

	private boolean inference;

	private Collection<String> languages;
	protected IndexingConfiguration idxConfig;
	protected Field.Index labelAnalysisMode;

	/**
	 * TODO at the moment, URILangs are ignored
	 */
	// private Collection<String> URILangs;

	private String indexDir;

	private OWLModel model;

	IndexWriterMap idxWritersMap;
	AnalyzerMap idxAnalyzerMap;

	public RDFIndexManager(OWLModel m, String IndexDir, IndexingConfiguration idxConfig) {
		this.indexDir = IndexDir;
		this.model = m;

		this.inference = false;		

		this.idxConfig = idxConfig;
		labelAnalysisMode = idxConfig.areLabelsTokenized() ? Index.ANALYZED : Index.NOT_ANALYZED;
		this.languages = idxConfig.getIndexedLanguages();

		idxWritersMap = new IndexWriterMap();
		idxAnalyzerMap = new AnalyzerMap();
	}

	/**
	 * this is a generic initialization method which should be invoked to make sure that the IndexManager
	 * prepares the right IndexWriter for each managed index
	 * 
	 * @param ontTypes
	 *            ontology types which need to be indexed in the standard way
	 * @throws IOException
	 */
	public void initializeStandardIndexWriters(RDFResourceRolesEnum... ontTypes) throws IOException {
		if (ontTypes.length == 0) {
			ontTypes = OWLSearchEngine.allTypes;
		}

		for (RDFResourceRolesEnum type : ontTypes) {
			initializeWritersAndAnalyzersMap(type.toString(), languages, false);
		}
	}

	/**
	 * this is a generic initialization method which should be invoked to make sure that the IndexManager
	 * prepares the right IndexWriter for each managed index
	 * 
	 * @param idxIdentifiers
	 * @throws IOException
	 */
	public void initializeCustomIndexWriters(Collection<String> idxIdentifiers) throws IOException {
		for (String idxIdentifier : idxIdentifiers) {
			initializeWritersAndAnalyzersMap(idxIdentifier, languages, false);
		}
	}

	/**
	 * index the ontology for given <code>ontTypes</code>
	 * 
	 * @param create
	 * @param ontTypes
	 *            a list of ontology types to be annotated among (cls, property and individual)
	 * @throws IOException
	 * @throws ModelAccessException
	 * @throws OntoTypeNotIndexableException
	 */
	public void buildIndex(boolean create, RDFResourceRolesEnum... ontTypes) throws IOException,
			ModelAccessException, OntoTypeNotIndexableException {

		if (ontTypes.length == 0) {
			ontTypes = OWLSearchEngine.allTypes;
		}

		for (RDFResourceRolesEnum type : ontTypes) {

			ARTURIResourceIterator resourcesIterator;
			if (type == RDFResourceRolesEnum.cls)
				resourcesIterator = model.listNamedClasses(inference);
			else if (type == RDFResourceRolesEnum.property)
				resourcesIterator = model.listProperties();
			else if (type == RDFResourceRolesEnum.individual)
				resourcesIterator = model.listNamedInstances();
			else
				throw new OntoTypeNotIndexableException(type);

			logger.info("\nBUILDING INDEX FOR RESOURCE TYPE: " + type + "\n");
			buildIndexForResources(type.toString(), resourcesIterator, RDFS.Res.LABEL, create);

			logger.info(type + " INDEX BUILT");
		}
	}

	/**
	 * index resources returned by iterator <code>resourcesIterator</code>
	 * 
	 * @param idxIdentifier
	 * @param resourcesIterator
	 * @param create
	 * @throws IOException
	 * @throws ModelAccessException
	 */
	public void buildIndex(String idxIdentifier, ARTURIResourceIterator resourcesIterator,
			ARTURIResource predicate, boolean create) throws IOException, ModelAccessException {
		buildIndexForResources(idxIdentifier, resourcesIterator, predicate, create);
		logger.debug(idxIdentifier + " INDEX BUILT");
	}

	// INDEX IDENTIFIERS MANAGEMENT

	/**
	 * gets an index identifier for a given language index, starting from a neutral identifier and a given
	 * language
	 * 
	 * @param rootIdxIdentifier
	 * @param lang
	 * @return
	 */
	protected String getIndexDirName(String rootIdxIdentifier, String lang) {
		return rootIdxIdentifier + "_" + lang;
	}

	/**
	 * gets an index identifier for a given language index associated to a given resource type
	 * 
	 * @param type
	 * @param lang
	 * @return
	 */
	protected String getIndexDirName(RDFResourceRolesEnum type, String lang) {
		return getIndexDirName(type.toString(), lang);
	}

	// INDEX DIRECTORIES MANAGEMENT

	public String getBaseIndexDir() {
		return indexDir;
	}

	public String getIndexDir(String idxIdentifier) {
		return indexDir + "/" + idxIdentifier;
	}

	public String getIndexDir(RDFResourceRolesEnum type) {
		return getIndexDir(type.toString());
	}

	public String getLangIndexDirForTypeLabels(String idxIdentifier, String lang) {
		return getIndexDir(getIndexDirName(idxIdentifier, lang));
	}

	public String getLangIndexDirForTypeLabels(RDFResourceRolesEnum type, String lang) {
		return getIndexDir(getIndexDirName(type.toString(), lang));
	}

	public Directory getIndexDirectory(String idxIdentifier) throws IOException {
		return new SimpleFSDirectory(new File(indexDir + "/" + idxIdentifier));
	}

	public Directory getLangIndexDirectoryForTypeLabels(String idxIdentifier, String lang) throws IOException {
		return new SimpleFSDirectory(new File(getIndexDir(getIndexDirName(idxIdentifier, lang))));
	}

	// ////////////

	public IndexWriter getIndexWriter(String idxIdentifier) {
		return idxWritersMap.get(idxIdentifier);
	}

	/**
	 * generic method for retrieving index writers<br/>
	 * given an index identifier and a language, it retrieves the associated index writer
	 * 
	 * @param idxIdentifier
	 * @param lang
	 * @return
	 */
	public IndexWriter getIndexWriter(String idxIdentifier, String lang) {
		return idxWritersMap.get(idxIdentifier, lang);
	}

	/**
	 * given the type of resource which is searched and the language of its indexed labels, it returns the
	 * associated index writer
	 * 
	 * @param type
	 * @param lang
	 * @return
	 */
	public IndexWriter getIndexWriter(RDFResourceRolesEnum type, String lang) {
		return getIndexWriter(type.toString(), lang);
	}

	public Analyzer getIndexAnalyzer(String lang) {
		return idxAnalyzerMap.get(lang);
	}

	public Analyzer prepareSuitableAnalyzer(String lang) {
		Analyzer analyzer = idxConfig.getAnalyzerForLabels(lang);
		idxAnalyzerMap.put(lang, analyzer);
		return analyzer;
	}

	private Analyzer getRDFURIAnalyzer() {
		// return new StarredGenericTokenAnalyzer();
		return new KeywordAnalyzer();
	}

	// TODO this one has to be modified (see NOTES.TXT) to produce one single index. Also URILang
	// (i.e. the n.language used to represent the localnames of resources) must be taken into account.
	/**
	 * This method generates the indexes for the various languages. It first creates a lucene document, then
	 * adds the localName field to that document, then creates other documents for each of the available
	 * languages document to the specific index
	 * 
	 * @param idxIdentifier
	 * @param resourcesIterator
	 * @param property
	 * @param create
	 * @throws IOException
	 * @throws ModelAccessException
	 */
	private void buildIndexForResources(String idxIdentifier, ARTURIResourceIterator resourcesIterator,
			ARTURIResource property, boolean create) throws IOException, ModelAccessException {
		Document resIdx;

		initializeWritersAndAnalyzersMap(idxIdentifier, languages, create);

		IndexWriter mainWriter = getIndexWriter(idxIdentifier);
		int i = 0;
		while (resourcesIterator.hasNext()) {
			i++;
			logger.debug("resource #" + i);

			ARTURIResource ontResource = (ARTURIResource) resourcesIterator.next();

			// this part is useless because it adds URI and localname to a centralindex which is never used
			// (see NOTES.TXT)
			resIdx = new Document();
			logger.debug("checking stuff to add for resource: " + ontResource);
			createRDFResourceMetadataIndexingInfo(resIdx, ontResource);
			mainWriter.addDocument(resIdx);

			// end of useless part

			for (String lang : languages) {
				indexRDFResourceForSpecificLang(ontResource, property, idxIdentifier, lang);
			}

		}

		// closing main writer (related to only uri and local name
		// mainWriter.optimize();
		mainWriter.close();

		// closing language/labels writers
		for (String lang : languages) {
			IndexWriter idxWriter = idxWritersMap.get(idxIdentifier, lang);
			// idxWriter.optimize();
			idxWriter.close();
		}

		logger.debug("indexbuilt");
	}

	/**
	 * An IndexManager contains two maps for Analyzers and Writers. These two maps need to be filled with
	 * instances of analyzers and writers for each specific pair of index identifier (specified as a String)
	 * and language.<br/>
	 * Each time this method is invoked, the two maps are reset, their writers/analyzers are closed and
	 * reinitialized
	 * 
	 * @param idxIdentifier
	 * @param languages
	 * @param create
	 * @throws IOException
	 */
	private void initializeWritersAndAnalyzersMap(String idxIdentifier, Collection<String> languages,
			boolean create) throws IOException {

		logger.debug("initializing writers and analyzers for: " + idxIdentifier);

		// ***********************************************
		// first prepares the index for URI and localnames
		// ***********************************************

		// first checks if a writer is already present, and in case closes it
		resetWriter(idxIdentifier);

		Analyzer analyzer = getRDFURIAnalyzer();
		idxAnalyzerMap.put(idxIdentifier, analyzer);
		File indexDir = new File(getIndexDir(idxIdentifier));

		IndexWriter idxWrt = createIndexWriter(indexDir, analyzer, create);
		idxWritersMap.put(idxIdentifier, idxWrt);
		// idxWrt.close();

		logger.debug("generic writer initialized for: " + idxIdentifier);
		logger.debug("now initializing writers for languages: " + languages);

		// ***********************************************
		// then the indices for labeled info
		// ***********************************************

		Iterator<String> langIt = languages.iterator();
		while (langIt.hasNext()) {
			String lang = langIt.next();

			// first checks if a writer is already present, and in case closes it
			resetWriter(idxIdentifier, lang);

			analyzer = idxAnalyzerMap.get(lang);
			if (analyzer == null) {
				analyzer = prepareSuitableAnalyzer(lang);
			}
			indexDir = new File(getLangIndexDirForTypeLabels(idxIdentifier, lang));

			idxWrt = createIndexWriter(indexDir, analyzer, create);
			idxWritersMap.put(idxIdentifier, lang, idxWrt);
			// idxWrt.close();

			logger.debug("writer for language: " + lang + " initialized for base index: " + idxIdentifier);
		}

		logger.debug("all index managers have been initialized");

	}

	private IndexWriter createIndexWriter(File indexDir, Analyzer analyzer, boolean create)
			throws IOException {
		OpenMode openMode;

		// if index does not exist, create it in any case, otherwise, follow the value of the create argument
		if (create)
			openMode = OpenMode.CREATE;
		else
			openMode = OpenMode.CREATE_OR_APPEND;

		Directory indexDirectory = new SimpleFSDirectory(indexDir);
		IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_36, analyzer);
		config.setOpenMode(openMode);
		IndexWriter idxWrt = new IndexWriter(indexDirectory, config);
		return idxWrt;
	}

	// checks for existance of a writer and in case closes it
	private void resetWriter(String idxIdentifier) throws CorruptIndexException, IOException {
		IndexWriter idxWrt = idxWritersMap.get(idxIdentifier);
		if (idxWrt != null) {
			logger.debug(idxIdentifier + " writer already present, trying to close it...");
			idxWrt.close();
		}
	}

	// checks for existance of a writer and in case closes it
	private void resetWriter(String idxIdentifier, String lang) throws CorruptIndexException, IOException {
		IndexWriter idxWrt = idxWritersMap.get(idxIdentifier, lang);
		if (idxWrt != null) {
			logger.debug(getIndexDirName(idxIdentifier, lang)
					+ " language writer already present, trying to close it...");
			idxWrt.close();
		}
	}

	public IndexWriterMap getIdxWritersMap() {
		return idxWritersMap;
	}

	// INDEX MODIFICATION METHODS

	/**
	 * this method must be invoked when a new resource related to a specific index is being added to the
	 * manager ontology model
	 * 
	 * @param ontResource
	 * @param idxIdentifier
	 * @throws IOException
	 */
	public void addNewResourceToIndex(ARTURIResource ontResource, String idxIdentifier) throws IOException {
		// this part is useless because it adds URI and localname to a centralindex which is never used
		// (see NOTES.TXT)
		Document resIdx = new Document();
		logger.debug("checking stuff to add for resurce: " + ontResource);
		createRDFResourceMetadataIndexingInfo(resIdx, ontResource);
		IndexWriter mainWriter = getIndexWriter(idxIdentifier);
		logger.debug("main Writer: " + mainWriter.getDirectory());
		mainWriter.addDocument(resIdx);
		// closing main writer (related to only uri and local name
		mainWriter.close();
		// end of useless part
	}

	// RESOURCE DELETION FROM INDEXES

	/**
	 * to be invoked when a resource is being removed from the managed ontology model. Both the URI, localname
	 * and all labels for each language are removed from the multilingual indexes
	 * 
	 * @param ontResource
	 * @param idxIdentifier
	 * @throws IOException
	 */
	public void deleteResourceFromIndexes(ARTURIResource ontResource, String idxIdentifier)
			throws IOException {
		deleteResourceFromMainIndex(ontResource, idxIdentifier);
		for (String lang : languages) {
			deleteResourceFromLangIndex(ontResource, idxIdentifier, lang);
		}
	}

	// internal use only, invoked by deleteResourceFromIndexes
	protected void deleteResourceFromMainIndex(ARTURIResource ontResource, String idxIdentifier)
			throws IOException {
		IndexReader idxReader = IndexReader.open(getIndexDirectory(idxIdentifier));
		idxReader.deleteDocuments(new Term(idxFieldURI, ontResource.getURI()));
		idxReader.close();
	}

	// internal use only, invoked by deleteResourceFromIndexes
	protected void deleteResourceFromLangIndex(ARTURIResource ontResource, String idxIdentifier, String lang)
			throws IOException {
		IndexReader idxReader = IndexReader.open(getLangIndexDirectoryForTypeLabels(idxIdentifier, lang));
		idxReader.deleteDocuments(new Term(idxFieldURI, ontResource.getURI()));
		idxReader.close();
	}

	// LANGUAGE UPDATE IN RESOURCES

	/**
	 * if a value of property <code>property</code> in a given language <code>lang</code> is added/removed
	 * to/from resource <code>ontResource</code>, then this method can be used to realign the specific
	 * language index accordingly
	 * 
	 * @param ontResource
	 * @param idxIdentifier
	 * @param lang
	 * @throws IOException
	 * @throws ModelAccessException
	 */
	public void updateLangIndexForResource(ARTURIResource ontResource, ARTURIResource property,
			String idxIdentifier, String lang) throws IOException, ModelAccessException {

		logger.debug("updating resource: " + ontResource + " language content for" + lang);

		// NOTE: the document from the main index is not removed and recreated, as it has not substantially
		// changed. Only the document from the specific index is being removed here...
		deleteResourceFromLangIndex(ontResource, idxIdentifier, lang);

		// and recreated here
		indexRDFResourceForSpecificLang(ontResource, property, idxIdentifier, lang);

		// writer.close(); // this was closed still before modifications on 2013-01. But suppose an update
		// should not close a writer
	}

	/**
	 * shortcut for {@link #updateLangIndexForResource(ARTURIResource, ARTURIResource, String, String)} which
	 * uses {@link RDFS.Res#LABEL} as parameter for the property to be
	 * 
	 * @param ontResource
	 * @param idxIdentifier
	 * @param lang
	 * @throws IOException
	 * @throws ModelAccessException
	 */
	public void updateLangIndexForResource(ARTURIResource ontResource, String idxIdentifier, String lang)
			throws IOException, ModelAccessException {
		updateLangIndexForResource(ontResource, RDFS.Res.LABEL, idxIdentifier, lang);
	}

	// SMALL ATOMIC METHODS

	protected void createRDFResourceMetadataIndexingInfo(Document resIdx, ARTURIResource ontResource) {
		resIdx.add(new Field(idxFieldURI, ontResource.getURI(), Field.Store.YES, Field.Index.NOT_ANALYZED));
		logger.debug("ontresource: " + ontResource.getURI());
		resIdx.add(new Field(idxFieldLocalName, ontResource.getLocalName(), Field.Store.YES,
				Field.Index.NOT_ANALYZED));
		logger.debug("localname: " + ontResource.getLocalName());

	}

	/**
	 * @param resIdx
	 *            the Lucene document associated to the indexed resource
	 * @param ontResource
	 *            the resource to be indexed
	 * @param langLabelIt
	 *            an iterator over literals of a specific language describing the indexed resource
	 * @param lang
	 */
	protected void createRDFResourceLabelsIndexingInfo(Document resIdx, ARTURIResource ontResource,
			Iterator<ARTLiteral> langLabelIt, String lang) {
		String termVector = "";
		// String termVector = filteredLangIt.next().getLabel();

		logger.debug("term vector: " + termVector);
		while (langLabelIt.hasNext()) {
			termVector += _labelSplittingSeq + langLabelIt.next().getLabel();
		}

		resIdx.add(new Field(lang, termVector, Field.Store.YES, labelAnalysisMode));
		logger.debug("adding: " + termVector);
	}

	// TODO MAYBE not very efficient, because it gets around all the triples FOR ALL languages in the
	// languages list. Modify it, but take into consideration that the difficulty is in the fact
	// that each document is prepared as a single entity, IN ONE LANGUAGE.
	// at least, one very easy modification is to get all the labels one time, put them in a
	// collection, and then just parse the produced collection each time, instead of keeping reasking
	// it to the triplestore. Note that a specific MultiMap should contain all the termVectors for
	// each language and we should create/increment each of them for each lable found
	protected void indexRDFResourceForSpecificLang(ARTURIResource ontResource, ARTURIResource property,
			String idxIdentifier, String lang) throws ModelAccessException, CorruptIndexException,
			IOException {

		logger.debug("checking language: " + lang + " for: " + ontResource);
		IndexWriter writer = idxWritersMap.get(idxIdentifier, lang);

		ARTLiteralIterator labelsIterator = model.listValuesOfSubjDTypePropertyPair(ontResource, property,
				inference);
		Iterator<ARTLiteral> filteredLangIt = Iterators.filter(labelsIterator,
				new DesiredLanguageLabelPredicate(lang));

		if (filteredLangIt.hasNext()) {
			Document resIdx = new Document();

			createRDFResourceMetadataIndexingInfo(resIdx, ontResource);

			createRDFResourceLabelsIndexingInfo(resIdx, ontResource, filteredLangIt, lang);

			writer.addDocument(resIdx);
		}

		labelsIterator.close();

	}

}
